/*
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.dspl.validate.validator;

import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.google.dspl.model.Attribute;
import com.google.dspl.model.Concept;
import com.google.dspl.model.Concept.ConceptProperty;
import com.google.dspl.model.Concept.ConceptTableMapping;
import com.google.dspl.model.Concept.PropertyMapping;
import com.google.dspl.model.DataType;
import com.google.dspl.model.Dataset;
import com.google.dspl.model.Identifier;
import com.google.dspl.model.Slice;
import com.google.dspl.model.Table;
import com.google.dspl.model.Table.Column;
import com.google.dspl.validate.DatasetResolver;
import com.google.dspl.validate.ItemReference;
import com.google.dspl.validate.ValidationContext;
import com.google.dspl.validate.violation.ConceptConflictingDefaultValueTypeViolation;
import com.google.dspl.validate.violation.ConceptConflictingParentTypeViolation;
import com.google.dspl.validate.violation.ConceptInheritanceCycleViolation;
import com.google.dspl.validate.violation.ConceptMappingColumnNotFoundViolation;
import com.google.dspl.validate.violation.ConceptMappingConflictingColumnTypeViolation;
import com.google.dspl.validate.violation.ConceptMappingConflictingPropertyColumnTypeViolation;
import com.google.dspl.validate.violation.ConceptMappingPropertyColumnNotFoundViolation;
import com.google.dspl.validate.violation.ConceptMappingTableNotFoundViolation;
import com.google.dspl.validate.violation.ConceptNoIdViolation;
import com.google.dspl.validate.violation.ConceptNoTypeViolation;
import com.google.dspl.validate.violation.ConceptParentConceptNotFoundViolation;
import com.google.dspl.validate.violation.ConceptTopicNotFoundViolation;
import com.google.dspl.validate.violation.ConceptUnusedViolation;
import com.google.dspl.validate.violation.EntityConceptNoNamePropertyMappingViolation;
import com.google.dspl.validate.violation.Violation;

import java.util.List;
import java.util.Set;

/**
 * {@link Validator} for {@link Concept}s.
 *
 * @author Shardul Deo
 */
public class ConceptValidator implements Validator<Concept> {
  private static final Identifier ENTITY_ID = new Identifier("entity", "entity");

  @Override
  public List<Violation> validate(Concept concept, ValidationContext context) {
    ImmutableList.Builder<Violation> violations = ImmutableList.<Violation>builder();

    validateId(concept, context, violations);
    validateInfo(concept, context, violations);
    validateParent(concept, context, violations);
    validateType(concept, context, violations);
    validateTopicReferences(concept, context, violations);
    validateTableMapping(concept, context, violations);
    validateDefaultValueMatchesType(concept, context, violations);
    validateUsedInSlice(concept, context, violations);

    UniqueIdViolationAccumulator uniqueIdValidator =
        new UniqueIdViolationAccumulator();
    validateProperties(concept, context, violations, uniqueIdValidator);
    validateEntityNameProperty(concept, context, violations);
    validateAttributes(concept, context, violations, uniqueIdValidator);
    violations.addAll(uniqueIdValidator.getViolations());

    return violations.build();
 }

  private void validateParent(
      Concept concept, ValidationContext context, ImmutableList.Builder<Violation> violations) {
    if (concept.getParent() != null) {
      if (!ValidationHelper.isValidConceptReference(concept.getParent(), context)) {
        ItemReference conceptReference = conceptReference(concept, context);
        violations.add(new ConceptParentConceptNotFoundViolation(
            conceptReference, concept.getId(), concept.getParent()));
      }

      Set<ItemReference> visitedConcepts = Sets.newLinkedHashSet();
      Dataset dataset = context.getCurrentDataset();
      ItemReference conceptReference = ItemReference.reference(dataset, concept);
      visitedConcepts.add(conceptReference);

      Identifier parentId = concept.getParent();
      while (parentId != null) {
        Concept parentConcept = context.getDatasetResolver().getConcept(parentId);
        if (parentConcept == null) {
          break;
        }
        Dataset parentConceptDataset =
            context.getDatasetResolver().getDataset(parentId.getDatasetId());
        ItemReference parentReference =
            ItemReference.reference(parentConceptDataset, parentConcept);

        if (visitedConcepts.contains(parentReference)) {
          // So that we don't generate too many violations, we only report a violation if the
          // concept is part of a cycle, not if it points to a cycle. The latter case is
          // also problematic but we will generate violations for concepts in the cycle itself
          // anyway.
          if (parentReference.equals(conceptReference)) {
            violations.add(new ConceptInheritanceCycleViolation(
                conceptReference,
                ImmutableList.copyOf(visitedConcepts),
                concept.getId()));
          }
          break;
        }

        visitedConcepts.add(parentReference);
        parentId = parentConcept.getParent();
      }
    }
  }

  private void validateProperties(Concept concept, ValidationContext context,
      ImmutableList.Builder<Violation> result,
      UniqueIdViolationAccumulator uniqueIdValidator) {
    ConceptPropertyValidator propertyValidator =
        new ConceptPropertyValidator(concept);
    for (ConceptProperty property : concept.getProperties()) {
      ItemReference propertyItemReference =
          ItemReference.reference(context.getCurrentDataset(), concept, property);
      if (Strings.isNullOrEmpty(property.getId()) && property.getConcept() != null) {
        uniqueIdValidator.addId(property.getConcept().getObjectId(), propertyItemReference);
      } else {
        uniqueIdValidator.addId(property.getId(), propertyItemReference);
      }

      result.addAll(propertyValidator.validate(property, context));
    }
  }

  private void validateAttributes(Concept concept, ValidationContext context,
      ImmutableList.Builder<Violation> violations,
      UniqueIdViolationAccumulator uniqueIdValidator) {
    ConceptAttributeValidator attributeValidator =
        new ConceptAttributeValidator(concept);
    for (Attribute attribute : concept.getAttributes()) {
      ItemReference propertyItemReference =
          ItemReference.reference(context.getCurrentDataset(), concept, attribute);
      if (Strings.isNullOrEmpty(attribute.getId()) && attribute.getConcept() != null) {
        uniqueIdValidator.addId(attribute.getConcept().getObjectId(), propertyItemReference);
      } else {
        uniqueIdValidator.addId(attribute.getId(), propertyItemReference);
      }
      violations.addAll(attributeValidator.validate(attribute, context));
    }
  }

  private void validateEntityNameProperty(Concept concept, ValidationContext context,
      ImmutableList.Builder<Violation> violations) {
    DatasetResolver resolver = context.getDatasetResolver();
    if (resolver.isParent(context.getCurrentDataset().getDatasetId(), concept, ENTITY_ID)) {
      ConceptTableMapping tableMapping = concept.getTable();
      if (tableMapping != null) {
        boolean foundName = false;
        Multimap<String, PropertyMapping> explicitPropertyMappings = getPropertyMappings(concept);
        if (explicitPropertyMappings.containsKey("name")) {
          foundName = true;
        } else {
          Table definingTable = context.getDatasetResolver().getTable(
              new Identifier(context.getCurrentDataset().getDatasetId(), tableMapping.getTableId()));
          if (definingTable != null) {
            for (Column column : definingTable.getColumns()) {
              if ("name".equals(column.getId())) {
                foundName = true;
                break;
              }
            }
          }
        }
        if (!foundName) {
          ItemReference conceptReference =
              ItemReference.reference(context.getCurrentDataset(), concept);
          violations.add(
              new EntityConceptNoNamePropertyMappingViolation(conceptReference, concept.getId()));
        }
      }
    }
  }

  private void validateId(Concept concept, ValidationContext context,
      ImmutableList.Builder<Violation> violations) {
    ItemReference conceptReference = conceptReference(concept, context);

    if (Strings.isNullOrEmpty(concept.getId())) {
      violations.add(new ConceptNoIdViolation(conceptReference));
    } else {
      IdValidator validator = new IdValidator(conceptReference);
      validator.validate(concept.getId(), violations);
    }
  }

  private void validateInfo(Concept concept, ValidationContext context,
      ImmutableList.Builder<Violation> violations) {
    InfoValidator validator = new InfoValidator(conceptReference(concept, context), true);
    validator.validate(concept.getInfo(), violations);
  }

  private void validateType(
      Concept concept, ValidationContext context, ImmutableList.Builder<Violation> violations) {
    Identifier parentConceptId = concept.getParent();
    if (concept.getType() != null && parentConceptId != null) {
      Concept parent = context.getDatasetResolver().getConcept(parentConceptId);
      if (parent != null) {
        DataType superType = context.getDatasetResolver().resolveConceptType(
            parentConceptId.getDatasetId(), parent);
        if (superType != null
            && !ValidationHelper.isMoreRestrictiveType(concept.getType(), superType)) {
          violations.add(new ConceptConflictingParentTypeViolation(
              conceptReference(concept, context),
              concept.getId(), concept.getType(), parentConceptId,
              superType, ValidationHelper.getMoreRestrictiveTypes(superType)));
        }
      }
    }
    if (concept.getType() == null && parentConceptId == null) {
      violations.add(
          new ConceptNoTypeViolation(conceptReference(concept, context), concept.getId()));
    }
  }

  private void validateTopicReferences(
      Concept concept, ValidationContext context, ImmutableList.Builder<Violation> violations) {
    for (Identifier topicId : concept.getTopics()) {
      if (!ValidationHelper.isValidTopicReference(topicId, context)) {
        violations.add(new ConceptTopicNotFoundViolation(
            conceptReference(concept, context),
            concept.getId(),
            topicId));
      }
    }
  }

  private void validateTableMapping(
      Concept concept, ValidationContext context, ImmutableList.Builder<Violation> violations) {
    if (concept.getTable() != null && concept.getTable().getTableId() != null) {
      Identifier tableId = new Identifier(
          context.getCurrentDataset().getDatasetId(), concept.getTable().getTableId());

      if (!ValidationHelper.isValidTableReference(tableId, context)) {
        violations.add(new ConceptMappingTableNotFoundViolation(
            conceptReference(concept, context), concept.getId(), concept.getTable().getTableId()));
      } else {
        validateConceptMapping(concept, context, violations);

        Multimap<String, PropertyMapping> explicitPropertyMappings = getPropertyMappings(concept);

        // TODO: check that the same column isn't mapped twice?
        Set<String> explicitlyMappedColumns = Sets.newHashSet();
        for (PropertyMapping mapping : explicitPropertyMappings.values()) {
          if (mapping.getToColumn() != null) {
            explicitlyMappedColumns.add(mapping.getToColumn());
          }
        }

        for (String propertyId : getConceptPropertyIds(concept.getId(), context)) {
          validateConceptPropertyMapping(concept, propertyId, explicitPropertyMappings,
              explicitlyMappedColumns, context, violations);
        }
      }
    }
  }

  private Multimap<String, PropertyMapping> getPropertyMappings(Concept concept) {
    return Multimaps.index(
        concept.getTable().getPropertyMappings(), new Function<PropertyMapping, String>() {
            @Override
          public String apply(PropertyMapping mapping) {
            return Strings.nullToEmpty(mapping.getPropertyId());
          }
        });
  }

  private Set<String> getConceptPropertyIds(String concept, ValidationContext context) {
    Identifier conceptId = new Identifier(context.getCurrentDataset().getDatasetId(), concept);

    Multimap<Identifier, ConceptProperty> allProperties =
        context.getDatasetResolver().getPropertiesIncludingInherited(conceptId);
    Set<String> localPropertyIds = Sets.newHashSet();
    for (ConceptProperty property : allProperties.values()) {
      if (property.getId() != null) {
        localPropertyIds.add(property.getId());
      }
    }
    return localPropertyIds;
  }

  private void validateConceptPropertyMapping(Concept concept, String propertyId,
      Multimap<String, PropertyMapping> propertyMappings, Set<String> explicitlyMappedColumns,
      ValidationContext context, ImmutableList.Builder<Violation> violations) {
    String datasetId = context.getCurrentDataset().getDatasetId();
    Identifier conceptId = new Identifier(datasetId, concept.getId());
    Identifier tableId = new Identifier(datasetId, concept.getTable().getTableId());

    ConceptProperty property =
        context.getDatasetResolver().getConceptProperty(conceptId, propertyId);
    ItemReference propertyReference = propertyReference(concept, property, context);

    for (PropertyMapping explicitMapping : propertyMappings.get(propertyId)) {
      // Validate explicit property mapping.
      // TODO: check that we don't have two mappings for the same language.
      if (explicitMapping.getToColumn() != null) {
        String columnId = explicitMapping.getToColumn();
        Column column = context.getDatasetResolver().getColumn(tableId, columnId);
        if (column == null) {
          violations.add(new ConceptMappingPropertyColumnNotFoundViolation(
              propertyReference,
              property.getId(),
              concept.getId(),
              columnId,
              tableId.getObjectId()));
        } else {
          validatePropertyMappingColumnType(concept, property, column, context, violations);
        }
      }
    }

    // Validate implicit property mapping.
    if (propertyMappings.get(propertyId).isEmpty()
        && !explicitlyMappedColumns.contains(propertyId)) {
      String columnId = propertyId;
      DatasetResolver resolver = context.getDatasetResolver();
      Column column = resolver.getColumn(tableId, columnId);
      // An implicit mapping doesn't have to exist, so we don't report a violation if the
      // column is null.
      if (column != null) {
        validatePropertyMappingColumnType(concept, property, column, context, violations);
      }
    }
  }

  private void validatePropertyMappingColumnType(Concept concept, ConceptProperty property,
      Column column, ValidationContext context, ImmutableList.Builder<Violation> violations) {
    String datasetId = context.getCurrentDataset().getDatasetId();
    Identifier tableId = new Identifier(datasetId, concept.getTable().getTableId());
    Identifier conceptId = new Identifier(datasetId, concept.getId());

    ItemReference propertyReference = propertyReference(concept, property, context);

    DataType columnType = column.getType();
    DataType propertyType = context.getDatasetResolver()
        .resolveConceptPropertyType(datasetId, property.getId(), concept);
    if (columnType != null && propertyType != null && columnType != propertyType) {
      violations.add(new ConceptMappingConflictingPropertyColumnTypeViolation(
          propertyReference,
          property.getId(),
          conceptId.getObjectId(),
          column.getId(),
          tableId.getObjectId(),
          propertyType,
          columnType));
    }
  }

  private void validateConceptMapping(
      Concept concept, ValidationContext context, ImmutableList.Builder<Violation> violations) {
    DatasetResolver resolver = context.getDatasetResolver();
    String datasetId = context.getCurrentDataset().getDatasetId();
    Identifier tableId = new Identifier(datasetId, concept.getTable().getTableId());

    String columnId = (concept.getTable().getConceptMappingColumn() != null)
        ? concept.getTable().getConceptMappingColumn()
        : concept.getId(); // Implicit mapping.

    if (columnId != null) {
      Column column = resolver.getColumn(tableId, columnId);
      if (column == null) {
        violations.add(new ConceptMappingColumnNotFoundViolation(
            conceptReference(concept, context),
            concept.getId(),
            columnId,
            tableId.getObjectId()));
      } else if (column.getType() != null) {
        DataType columnType = column.getType();
        DataType conceptType = resolver.resolveConceptType(datasetId, concept);

        if (conceptType != null && conceptType != columnType) {
          violations.add(new ConceptMappingConflictingColumnTypeViolation(
              conceptReference(concept, context),
              concept.getId(),
              columnId,
              tableId.getObjectId(),
              conceptType,
              columnType));
        }
      }
    }
  }

  private void validateDefaultValueMatchesType(
      Concept concept, ValidationContext context, ImmutableList.Builder<Violation> violations) {
    if (concept.getDefaultValue() != null) {
      String datasetId = context.getCurrentDataset().getDatasetId();
      DataType type = context.getDatasetResolver().resolveConceptType(datasetId, concept);
      if (type != null && !ValidationHelper.isValidValueOfType(concept.getDefaultValue(), type)) {
        violations.add(new ConceptConflictingDefaultValueTypeViolation(
            conceptReference(concept, context),
            concept.getId(),
            concept.getDefaultValue(),
            concept.getType()));
      }
    }
  }

  private void validateUsedInSlice(Concept concept, ValidationContext context,
      ImmutableList.Builder<Violation> violations) {
    Dataset dataset = context.getCurrentDataset();
    Identifier conceptId = new Identifier(dataset.getDatasetId(), concept.getId());
    boolean conceptUsedInSlice = false;
    for (Slice slice : dataset.getSlices()) {
      if (slice.getDimensions().contains(conceptId) || slice.getMetrics().contains(conceptId)) {
        conceptUsedInSlice = true;
        break;
      }
    }
    if (!conceptUsedInSlice) {
      violations.add(
          new ConceptUnusedViolation(conceptReference(concept, context), concept.getId()));
    }
  }

  private ItemReference conceptReference(Concept concept, ValidationContext context) {
    return ItemReference.reference(context.getCurrentDataset(), concept);
  }

  private ItemReference propertyReference(
      Concept concept, ConceptProperty property, ValidationContext context) {
    return ItemReference.reference(context.getCurrentDataset(), concept, property);
  }
}
